#!/usr/bin/env bash
# vim: set filetype=sh:

PREFIX="${DESK_DIR:-$HOME/.desk}"
DESKS="${DESK_DESKS_DIR:-$PREFIX/desks}"
DESKFILE_NAME=Deskfile


## Commands

cmd_version() {
    echo "◲  desk 0.6.0"
}


cmd_usage() {
    cmd_version
    echo
    cat <<_EOF
Usage:

    $PROGRAM
        List the current desk and any associated aliases. If no desk
        is being used, display available desks.
    $PROGRAM init
        Initialize desk configuration.
    $PROGRAM (list|ls)
        List all desks along with a description.
    $PROGRAM (.|go) [<desk-name-or-path> [shell-args...]]
        Activate a desk. Extra arguments are passed onto shell. If called with
        no arguments, look for a Deskfile in the current directory. If not a
        recognized desk, try as a path to directory containing a Deskfile.
    $PROGRAM run <desk-name> '<cmd>'
    $PROGRAM run <desk-name> <cmd> <arg>...
        Run a command within a desk's environment then exit. In the first form
        shell expansion is performed. In the second form, the argument vector
        is executed as is.
    $PROGRAM edit [desk-name]
        Edit (or create) a deskfile with the name specified, otherwise
        edit the active deskfile.
    $PROGRAM help
        Show this text.
    $PROGRAM version
        Show version information.

Since desk spawns a shell, to deactivate and "pop" out a desk, you
simply need to exit or otherwise end the current shell process.
_EOF
}

cmd_init() {
    if [ -d "$PREFIX" ]; then
        echo "Desk dir already exists at ${PREFIX}"
        exit 1
    fi
    read -p "Where do you want to store your deskfiles? (default: ${PREFIX}): " \
        NEW_PREFIX
    [ -z "${NEW_PREFIX}" ] && NEW_PREFIX="$PREFIX"

    if [ ! -d "${NEW_PREFIX}" ]; then
        echo "${NEW_PREFIX} doesn't exist, attempting to create."
        mkdir -p "$NEW_PREFIX/desks"
    fi

    local SHELLTYPE=$(get_running_shell)

    case "${SHELLTYPE}" in
        bash)   local SHELLRC="${HOME}/.bashrc" ;;
        fish)   local SHELLRC="${HOME}/.config/fish/config.fish" ;;
        zsh)    local SHELLRC="${HOME}/.zshrc" ;;
    esac

    read -p "Where's your shell rc file? (default: ${SHELLRC}): " \
        USER_SHELLRC
    [ -z "${USER_SHELLRC}" ] && USER_SHELLRC="$SHELLRC"
    if [ ! -f "$USER_SHELLRC" ]; then
        echo "${USER_SHELLRC} doesn't exist"
        exit 1
    fi

    printf "\n# Hook for desk activation\n" >> "$USER_SHELLRC"

    # Since the hook is appended to the rc file, its exit status becomes
    # the exit status of `source $USER_SHELLRC` which typically
    # indicates if something went wrong. If $DESK_ENV is void, `test`
    # sets exit status to 1. That, however, is part of desk's normal
    # operation, so we clear exit status after that.
    if [ "$SHELLTYPE" == "fish" ]; then
      echo "test -n \"\$DESK_ENV\"; and . \"\$DESK_ENV\"; or true" >> "$USER_SHELLRC"
    else
      echo "[ -n \"\$DESK_ENV\" ] && source \"\$DESK_ENV\" || true" >> "$USER_SHELLRC"
    fi

    echo "Done. Start adding desks to ${NEW_PREFIX}/desks!"
}


cmd_go() {
    # TODESK ($1) may either be
    #
    #   - the name of a desk in $DESKS/
    #   - a path to a Deskfile
    #   - a directory containing a Deskfile
    #   - empty -> `./Deskfile`
    #
    local TODESK="$1"
    local DESKEXT=$(get_deskfile_extension)
    local DESKPATH="$(find "${DESKS}/" -name "${TODESK}${DESKEXT}" 2>/dev/null)"

    local POSSIBLE_DESKFILE_DIR="${TODESK%$DESKFILE_NAME}"
    if [ -z "$POSSIBLE_DESKFILE_DIR" ]; then
      POSSIBLE_DESKFILE_DIR="."
    fi

    # If nothing could be found in $DESKS/, check to see if this is a path to
    # a Deskfile.
    if [[ -z "$DESKPATH" && -d "$POSSIBLE_DESKFILE_DIR" ]]; then
      if [ ! -f "${POSSIBLE_DESKFILE_DIR}/${DESKFILE_NAME}" ]; then
        echo "No Deskfile found in '${POSSIBLE_DESKFILE_DIR}'"
        exit 1
      fi

      local REALPATH=$( cd $POSSIBLE_DESKFILE_DIR && pwd )
      DESKPATH="${REALPATH}/${DESKFILE_NAME}"
      TODESK=$(basename "$REALPATH")
    fi

    # Shift desk name so we can forward on all arguments to the shell.
    shift;

    if [ -z "$DESKPATH" ]; then
        echo "Desk $TODESK (${TODESK}${DESKEXT}) not found in $DESKS"
        exit 1
    else
        local SHELL_EXEC="$(get_running_shell)"
        DESK_NAME="${TODESK}" DESK_ENV="${DESKPATH}" exec "${SHELL_EXEC}" "$@"
    fi
}


cmd_run() {
    local TODESK="$1"
    shift;
    if [ $# -eq 1 ]; then
        cmd_go "$TODESK" -ic "$1"
    else
        cmd_go "$TODESK" -ic '"$@"' -- "$@"
    fi
}


# Usage:        desk list [options]
# Description:  List all desks along with a description
# --only-names: List only the names of the desks
# --no-format:  Use ' - ' to separate names from descriptions
cmd_list() {
    if [ ! -d "${DESKS}/" ]; then
        echo "No desk dir! Run 'desk init'."
        exit 1
    fi
    local SHOW_DESCRIPTIONS DESKEXT AUTO_ALIGN name desc len longest out

    while [[ $# -gt 0 ]]; do
        case "$1" in
            --only-names) SHOW_DESCRIPTIONS=false && AUTO_ALIGN=false ;;
            --no-format) AUTO_ALIGN=false ;;
        esac
        shift
    done

    DESKEXT=$(get_deskfile_extension)
    out=""
    longest=0

    while read -d '' -r f; do
        name=$(basename "${f/${DESKEXT}//}")
        if [[ "$SHOW_DESCRIPTIONS" = false ]]; then
            out+="$name"$'\n'
        else
            desc=$(echo_description "$f")
            out+="$name - $desc"$'\n'
            len=${#name}
            (( len > longest )) && longest=$len
        fi
    done < <(find "${DESKS}/" -name "*${DESKEXT}" -print0)
    if [[ "$AUTO_ALIGN" != false ]]; then
        print_aligned "$out" "$longest"
    else
        printf "%s" "$out"
    fi
}

# Usage:	     desk [options]
# Description: List the current desk and any associated aliases. If no desk is being used, display available desks
# --no-format: Use ' - ' to separate alias/export/function names from their descriptions
cmd_current() {
    if [ -z "$DESK_ENV" ]; then
        printf "No desk activated.\n\n%s" "$(cmd_list)"
        exit 1
    fi

    local DESKPATH=$DESK_ENV
    local CALLABLES=$(get_callables "$DESKPATH")
    local AUTO_ALIGN len longest out

    while [[ $# -gt 0 ]]; do
        case "$1" in
            --no-format) AUTO_ALIGN=false ;;
        esac
        shift
    done
    printf "%s - %s\n" "$DESK_NAME" "$(echo_description "$DESKPATH")"

    if [[ -n "$CALLABLES" ]]; then

        longest=0
        out=$'\n'
        for NAME in $CALLABLES; do
            # Last clause in the grep regexp accounts for fish functions.
            len=$((${#NAME} + 2))
            (( len > longest )) && longest=$len
            local DOCLINE=$(
                grep -B 1 -E \
                    "^(alias ${NAME}=|export ${NAME}=|(function )?${NAME}( )?\()|function $NAME" "$DESKPATH" \
                    | grep "#")

            if [ -z "$DOCLINE" ]; then
                out+="  ${NAME}"$'\n'
            else
                out+="  ${NAME} - ${DOCLINE##\# }"$'\n'
            fi
        done

        if [[ "$AUTO_ALIGN" != false ]]; then
          print_aligned "$out" "$longest"
        else
          printf "%s" "$out"
        fi
    fi
}

cmd_edit() {
    if [ $# -eq 0 ]; then
        if [ "$DESK_NAME" == "" ]; then
            echo "No desk activated."
            exit 3
        fi
        local DESKNAME_TO_EDIT="$DESK_NAME"
    elif [ $# -eq 1 ]; then
        local DESKNAME_TO_EDIT="$1"
    else
        echo "Usage: ${PROGRAM} edit [desk-name]"
        exit 1
    fi

    local DESKEXT=$(get_deskfile_extension)
    local EDIT_PATH="${DESKS}/${DESKNAME_TO_EDIT}${DESKEXT}"

    ${EDITOR:-vi} "$EDIT_PATH"
}

## Utilities

FNAME_CHARS='[a-zA-Z0-9_-]'

# Echo the description of a desk. $1 is the deskfile.
echo_description() {
    local descline=$(grep -E "#\s+Description" "$1")
    echo "${descline##*Description: }"
}

# Echo a list of aliases, exports, and functions for a given desk
get_callables() {
    local DESKPATH=$1
    grep -E "^(alias |export |(function )?${FNAME_CHARS}+ ?\()|function $NAME" "$DESKPATH" \
        | sed 's/alias \([^= ]*\)=.*/\1/' \
        | sed 's/export \([^= ]*\)=.*/\1/' \
        | sed -E "s/(function )?(${FNAME_CHARS}+) ?\(\).*/\2/" \
        | sed -E "s/function (${FNAME_CHARS}+).*/\1/"
}

get_running_shell() {
    # Echo the name of the parent shell via procfs, if we have it available.
    # Otherwise, try to use ps with bash's parent pid.
    if [ -e /proc ]; then
        # Get cmdline procfile of the process running this script.
        local CMDLINE_FILE="/proc/$(grep PPid /proc/$$/status | cut -f2)/cmdline"

        if [ -f "$CMDLINE_FILE" ]; then
            # Strip out any verion that may be attached to the shell executable.
            # Strip leading dash for login shells.
            local CMDLINE_SHELL=$(sed -r -e 's/\x0.*//' -e 's/^-//' "$CMDLINE_FILE")
        fi
    else
        # Strip leading dash for login shells.
        # If the parent process is a login shell, guess bash.
        local CMDLINE_SHELL=$(ps -o comm= -p $PPID | tail -1 | sed -e 's/login/bash/' -e 's/^-//')
    fi

    if [ ! -z "$CMDLINE_SHELL" ]; then
        basename "$CMDLINE_SHELL"
        exit
    fi

    # Fall back to $SHELL otherwise.
    basename "$SHELL"
    exit
}

# Echo the extension of recognized deskfiles (.fish for fish)
get_deskfile_extension() {
    if [ "$(get_running_shell)" == "fish" ]; then
        echo '.fish'
    else
        echo '.sh'
    fi
}

# Echo first argument as key/value pairs aligned into two columns; second argument is the longest key
print_aligned() {
  local out=$1 longest=$2
  printf "%s" "$out" | awk -v padding="$longest" -F' - ' '{
    printf "%-*s %s\n", padding, $1, substr($0, index($0, " - ")+2, length($0))
  }'
}


PROGRAM="${0##*/}"

case "$1" in
    init) shift;               cmd_init "$@" ;;
    help|--help) shift;        cmd_usage "$@" ;;
    version|--version) shift;  cmd_version "$@" ;;
    ls|list) shift;            cmd_list "$@" ;;
    go|.) shift;               cmd_go "$@" ;;
    run) shift;                cmd_run "$@" ;;
    edit) shift;               cmd_edit "$@" ;;
    *)                         cmd_current "$@" ;;
esac
exit 0
